/**
 * Copyright (C) 2001-2017 by RapidMiner and the contributors
 * 
 * Complete list of developers available at our web site:
 * 
 * http://rapidminer.com
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.datatable;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.jfree.data.Range;

import com.rapidminer.gui.plotter.charts.AbstractChartPanel.Selection;
import com.rapidminer.report.Tableable;
import com.rapidminer.tools.Tools;
import com.rapidminer.tools.container.Pair;


/**
 * This abstract data table implementation provides some default implementations for data tables
 * like listener handling. The method {@link #fireEvent()} can be used to promote changes to all
 * listeners.
 *
 * In addition, IO methods are also provided by this abstract implementation.
 *
 * @author Ingo Mierswa
 */
public abstract class AbstractDataTable implements DataTable, Tableable {

	/** The list of data table listeners. */
	private List<WeakReference<DataTableListener>> weakReferencedListeners = new LinkedList<>();
	private List<DataTableListener> listeners = new LinkedList<>();

	/** The name of the table. */
	private String name;

	private HashSet<String> deselectionSet = new HashSet<>();

	private int deselectionCount;

	/**
	 * This is a constructor that will not set any name. It is used for serialization of subclasses.
	 */
	public AbstractDataTable() {
		this("");
	}

	public AbstractDataTable(String name) {
		this.name = name;
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String[] getColumnNames() {
		String[] result = new String[getNumberOfColumns()];
		for (int i = 0; i < result.length; i++) {
			result[i] = getColumnName(i);
		}
		return result;
	}

	@Override
	public void addDataTableListener(DataTableListener dataTableListener, boolean weakReference) {
		if (weakReference) {
			this.weakReferencedListeners.add(new WeakReference<>(dataTableListener));
		} else {
			addDataTableListener(dataTableListener);
		}
	}

	@Override
	public void addDataTableListener(DataTableListener dataTableListener) {
		listeners.add(dataTableListener);
	}

	@Override
	public void removeDataTableListener(DataTableListener dataTableListener) {
		Iterator<WeakReference<DataTableListener>> it = weakReferencedListeners.iterator();
		while (it.hasNext()) {
			DataTableListener l = it.next().get();
			if (l == null || l == dataTableListener) {
				it.remove();
			}
		}
		listeners.remove(dataTableListener);
	}

	protected void fireEvent() {
		// copy to avoid ConcurrentModification
		List<WeakReference<DataTableListener>> clone = new LinkedList<>(weakReferencedListeners);
		Iterator<WeakReference<DataTableListener>> i = clone.iterator();
		while (i.hasNext()) {
			WeakReference<DataTableListener> reference = i.next();
			DataTableListener listener = reference.get();
			if (listener != null) {
				listener.dataTableUpdated(this);
			} else {
				weakReferencedListeners.remove(reference);
			}
		}

		for (DataTableListener l : listeners) {
			l.dataTableUpdated(this);
		}
	}

	@Override
	public String getValueAsString(DataTableRow row, int column) {
		final double value = row.getValue(column);
		if (Double.isNaN(value)) {
			return null;
		} else if (isDate(column)) {
			return Tools.formatDate(new Date((long) value));
		} else if (isDateTime(column)) {
			return Tools.formatDateTime(new Date((long) value));
		} else if (isTime(column)) {
			return Tools.formatTime(new Date((long) value));
		} else if (isNominal(column)) {
			return mapIndex(column, (int) value);
		} else {
			return value + "";
		}
	}

	@Override
	public void write(PrintWriter out) throws IOException {
		out.println("# Generated by " + getName() + "[" + getClass().getName() + "]");
		for (int j = 0; j < getNumberOfColumns(); j++) {
			out.print((j != 0 ? "\t" : "# ") + getColumnName(j));
		}
		out.println();

		for (DataTableRow row : this) {
			for (int j = 0; j < getNumberOfColumns(); j++) {
				out.print((j != 0 ? "\t" : "") + getValueAsString(row, j));
			}
			out.println();
		}

		out.flush();
	}

	@Override
	public boolean containsMissingValues() {
		Iterator<DataTableRow> i = iterator();
		while (i.hasNext()) {
			DataTableRow row = i.next();
			for (int j = 0; j < getNumberOfColumns(); j++) {
				if (Double.isNaN(row.getValue(j))) {
					return true;
				}
			}
		}
		return false;
	}

	@Override
	public int getRowNumber() {
		return getNumberOfRows();
	}

	@Override
	public int getColumnNumber() {
		return getNumberOfColumns();
	}

	@Override
	public String getCell(int row, int column) {
		double value = getRow(row).getValue(column);
		if (isDate(column)) {
			return Tools.formatDate(new Date((long) value));
		} else if (isDateTime(column)) {
			return Tools.formatDateTime(new Date((long) value));
		} else if (isTime(column)) {
			return Tools.formatTime(new Date((long) value));
		} else if (isNominal(column)) {
			return mapIndex(column, (int) value);
		} else {
			return Tools.formatIntegerIfPossible(value);
		}
	}

	@Override
	public void prepareReporting() {}

	@Override
	public void finishReporting() {}

	@Override
	public boolean isFirstLineHeader() {
		return false;
	}

	@Override
	public boolean isFirstColumnHeader() {
		return false;
	}

	@Override
	public boolean isDeselected(String id) {
		return deselectionSet.contains(id);
	}

	@Override
	public void setSelection(Selection selection) {
		deselectionCount = 0;
		Collection<Pair<String, Range>> delimiters = selection.getDelimiters();
		Iterator<DataTableRow> i = iterator();
		deselectionSet.clear();
		while (i.hasNext()) {
			DataTableRow row = i.next();
			boolean rowSelected = true;
			for (Pair<String, Range> delimiter : delimiters) {
				int col = getColumnIndex(delimiter.getFirst());
				if (col >= 0 && col < this.getNumberOfColumns()) {
					double value = row.getValue(col);
					if (Double.isNaN(value)) {
						continue;
					}
					rowSelected &= delimiter.getSecond().contains(value);
				}
			}
			if (!rowSelected) {
				String id = row.getId();
				if (id != null) {
					deselectionSet.add(id);
					deselectionCount++;
				}
			}
		}
		fireEvent();
	}

	@Override
	public int getSelectionCount() {
		return getNumberOfRows() - deselectionCount;
	}
}
